- JSON, format document
- MongoDB, base de données orientée documents
- Un (petit) peu de statistique
Crédits :
- The Star Wars API (http://swapi.co/)
- Wookieepedia (http://starwars.wikia.com/)
22 juin 2016
Crédits :
Pas de réelle définition mais quelques propriétés :
{
"nom": "Skywalker",
"prenom": "Luke",
"profession": "Chevalier Jedi"
}
<personnage> <nom>Skywalker</nom> <prenom>Luke</prenom> <profession>Chevalier Jedi</profession> </personnage>
---
personnage:
nom: Skywalker
prenom: Luke
profession: Chevalier Jedi
...Types de base :
true et false,[],{}.{
"nom": "Han Solo",
"taille": 1.85,
"poids": 80,
"estCool": true,
"vaisseaux": [
"Faucon Millenium",
"Navette impériale"
],
"maitre": null,
"enfant": {
"nom": "Solo",
"prenom": "Ben"
}
}De nombreux sites proposent des API pour faire des requêtes et retournent les résultats au format JSON.
Exemples sur Wookieepedia :
http://starwars.wikia.com/api/v1/Articles/List/?limit=25
http://starwars.wikia.com/api/v1/Articles/Details/?ids=406052&abstract=100
http://starwars.wikia.com/api/v1/Search/List/?query=r2d2&limit=10&minArticleQuality=95&batch=5
Ces interfaces peuvent être utilisées dans un navigateur (peu utile), en ligne de commande (cURL,…) et, bien sûr, avec R.
Le package jsonlite est :
RJSONIO (mais complétement reécrit depuis),library(jsonlite)
articles <- fromJSON("http://starwars.wikia.com/api/v1/Articles/List/?limit=25")
L'objet retourné par fromJSON est de type list et ses composants correspondent aux clés du format JSON.
typeof(articles)
## [1] "list"
names(articles)
## [1] "items" "basepath" "offset"
nrow(articles$items)
## [1] 25
Le package jsonlite met (presque) en place une bijection entre la structure des données dans un document JSON et l'objet R retourné par fromJSON.
obj <- list(nom="Dark Vador", cote="Obscur") toJSON(obj)
## {"nom":["Dark Vador"],"cote":["Obscur"]}
all.equal(obj, fromJSON(toJSON(obj)))
## [1] TRUE
obj <- data.frame(
nom=c("Luke", "Leia", "Dark Vador"),
taille=c(1.72, 1.50, 2.02),
obscur=c(FALSE, FALSE, TRUE),
stringsAsFactors=FALSE)
all.equal(obj, fromJSON(toJSON(obj)))
## [1] TRUE
toJSON(obj, pretty=TRUE)
## [
## {
## "nom": "Luke",
## "taille": 1.72,
## "obscur": false
## },
## {
## "nom": "Leia",
## "taille": 1.5,
## "obscur": false
## },
## {
## "nom": "Dark Vador",
## "taille": 2.02,
## "obscur": true
## }
## ]La "presque" bijection se cache dans des détails comme la non prise en charge de NaN ou Inf dans certains cas.
x <- data.frame(a=c(3.14, NaN, Inf)) fromJSON(toJSON(x))
## a ## 1 3.14 ## 2 NA ## 3 NA
x <- list(a=c(3.14, NaN, Inf)) fromJSON(toJSON(x))
## $a ## [1] 3.14 NaN Inf
Les valeurs manquantes ne sont tout simplement pas stockées.
x <- data.frame(star=c(FALSE, TRUE, NA, NA),
wars=c("Jedi", NA, NA, "Galaxy"))
toJSON(x)
## [{"star":false,"wars":"Jedi"},{"star":true},{},{"wars":"Galaxy"}]
Pour une discussion plus poussée sur cette "bijection" et de ses limites :
Jeroen Ooms, The jsonlite Package: A Practical and Consistent Mapping Between JSON Data and R Objects, 2014
La plupart du temps, les API limitent le nombre de réponses par requête et imposent l'utilisation de pages.
url <- "http://starwars.wikia.com/api/v1/Search/List/?query=r2d2&limit=10"
pages <- list()
for (i in 1:5) {
data_page <- fromJSON(paste0(url, "&batch=", i))
pages[[i]] <- data_page$items
}
items <- rbind.pages(pages)
nrow(items)
## [1] 50
Pour manipuler de grands volumes de données sous forme de flux, le package jsonlite propose les fonctions stream_in et stream_out. Celles-ci permettent de manipuler des données au format JSON "ligne par ligne".
stream_out(iris, stdout())
stream_out(iris, file("/tmp/iris.json"), verbose=FALSE)
stream_in(file("/tmp/iris.json"), pagesize=42, verbose=FALSE, handler=function(df) {
# Traitement des données
df <- df[df$Species == "virginica",]
message(nrow(df), " ligne(s) conservée(s)")
stream_out(df, file("/tmp/iris2.json"), verbose=FALSE)
})
## 0 ligne(s) conservée(s) ## 0 ligne(s) conservée(s)
## 26 ligne(s) conservée(s)
## 24 ligne(s) conservée(s)
Plus d'infos et d'exemples dans la doc…
T'entraîner seul,
maintenant, tu dois.
Et bien d'autres…
_id et organisés en collections.Afin d'interagir avec MongoDB depuis R, nous utiliserons le package mongolite qui dépend de jsonlite (même auteur, Jeroen Ooms) comme la Force dépend des midi-chloriens.
_id et organisés en collections.Afin d'interagir avec mongoDB depuis R, nous utiliserons le package mongolite qui dépend de jsonlite (même auteur, Jeroen Ooms) comme la Force dépend des midi-chloriens.
La fonction mongo permet de se connecter au serveur mongoDB (par défaut, localhost) et de récupérer un objet R pour interagir avec une collection donnée.
library(mongolite) col_people <- mongo(collection="people", verbose=FALSE)
La collection n'a pas besoin d'être préalablement créée et l'objet retourné par mongo offre plusieurs méthodes pour la manipuler. Par exemple, count sans paramètre retourne le nombre de documents.
col_people$count()
## [1] 0
La méthode insert permet d'insérer un data frame dans la collection.
obj <- fromJSON("http://swapi.co/api/people/")
col_people$insert(obj$results)
L'absence de schéma permet d'insérer des data frames qui n'ont pas la même structure.
col_people$insert(data.frame(name="Captain Kirk", alias="Kirky"))
col_people$count()
## [1] 11
find retourne les données correpondantes à certains critères passés au paramètre query.col_people$find()
col_people$find(query='{"gender":"male"}')
Pour limiter les champs retournés, on utilisera le paramètre fields.
col_people$find(query='{"gender":"male"}', fields='{"name":1}')
Par défaut, le champ _id est toujours retourné…
col_people$find(query='{"gender":"male"}', fields='{"_id":0, "name":1}')
## name ## 1 Luke Skywalker ## 2 Darth Vader ## 3 Owen Lars ## 4 Biggs Darklighter ## 5 Obi-Wan Kenobi
Il est possible d'utiliser des commandes dans le paramètre query. Celles-ci sont précédées du caractère '$'. Par exemple, $exists pour tester l'existence d'un champ :
col_people$find(query='{"gender": {"$exists":false} }', fields='{"_id":0, "name":1}')
La commande $regex permet d'utiliser des expressions régulières :
col_people$find(query='{"name": {"$regex":"^C"} }',
fields='{"_id":0, "name":1, "alias":1}')
## name alias ## 1 C-3PO <NA> ## 2 Captain Kirk Kirky
Le paramètre sort de la méthode find permet de trier les résultats selon les valeurs d'un champ (1 pour l'ordre croissant et -1 pour le décroissant).
col_people$find(fields='{"_id":0, "name":1}', sort='{"name":1}')
col_people$find(fields='{"_id":0, "name":1, "height":1}', sort='{"height":-1}')
df <- col_people$find(fields='{"_id":0, "name":1, "height":1}', sort='{"height":-1}')
print(df)
## name height ## 1 R5-D4 97 ## 2 R2-D2 96 ## 3 Darth Vader 202 ## 4 Biggs Darklighter 183 ## 5 Obi-Wan Kenobi 182 ## 6 Owen Lars 178 ## 7 Luke Skywalker 172 ## 8 C-3PO 167 ## 9 Beru Whitesun lars 165 ## 10 Leia Organa 150 ## 11 Captain Kirk <NA>
df <- col_people$find(fields='{"_id":0, "name":1, "height":1}', sort='{"height":-1}')
print(df)
## name height ## 1 R5-D4 97 ## 2 R2-D2 96 ## 3 Darth Vader 202 ## 4 Biggs Darklighter 183 ## 5 Obi-Wan Kenobi 182 ## 6 Owen Lars 178 ## 7 Luke Skywalker 172 ## 8 C-3PO 167 ## 9 Beru Whitesun lars 165 ## 10 Leia Organa 150 ## 11 Captain Kirk <NA>
str(df)
## 'data.frame': 11 obs. of 2 variables: ## $ name : chr "R5-D4" "R2-D2" "Darth Vader" "Biggs Darklighter" ... ## $ height: chr "97" "96" "202" "183" ...
La méthode remove peut être utilisée pour :
col_people$remove(query='{"gender":"male"}')
col_people$find()
col_people$remove(query='{"gender":"male"}', multiple=TRUE)
col_people$find()
La méthode drop permet de supprimer toute une collection.
col_people$drop() col_people$find(); col_people$count()
clean_df <- function(df) {
df$height <- suppressWarnings(as.double(df$height))
df$mass <- suppressWarnings(as.double(gsub(",", "", df$mass)))
df$films <- lapply(df$films,
function(films) { sort(as.integer(substr(films, 27, 27))) })
return(df)
}
Toutes les pages peuvent être récupérées (la connexion échoue parfois…)
i <- 0
repeat {
i <- i + 1
content <- fromJSON(paste0("http://swapi.co/api/people/?page=", i))
col_people$insert(clean_df(content$results))
if (is.null(content$`next`)) break
}
export permet d'exporter une collection vers un fichier JSON.col_people$export(file("/tmp/people.json"))
import permet d'importer une collection (utile pour les sauvegardes de petites collections ;-)col_people$drop()
col_people$count()
col_people$import(file("/tmp/people.json"))
col_people$count()
Le tri décroissant sur height est maintenant correct.
col_people$find(fields='{"_id":0, "name":1, "height":1}', sort='{"height":-1}')
Le paramètre limit permet de ne récupérer que les premiers éléments d'une réponse.
col_people$find(fields='{"_id":0, "name":1, "height":1}', sort='{"height":-1}', limit=5)
## name height ## 1 Yarael Poof 264 ## 2 Tarfful 234 ## 3 Lama Su 229 ## 4 Chewbacca 228 ## 5 Roos Tarpals 224
$lt, $lte, $gt et $gtecol_people$find(query='{"height": {"$gt":160} }',
fields='{"_id":0, "name":1, "height":1}')
$necol_people$find(query='{"gender": {"$ne": "male"} }',
fields='{"_id":0, "name":1, "gender":1}')
$and, $or, $nor et $notcol_people$find(query='{"$or": [{"gender" : "male"}, {"gender" : "female"}]}',
fields='{"_id":0, "name":1, "gender":1}')
$in et $nin# Les films sont dans l'ordre historique
ffilms <- '{"_id":0, "name":1, "films":1}'
col_people$find(query='{"films": {"$in": [1,2,3]} }', fields=ffilms)
col_people$find(query='{"films": {"$nin": [1,2,3]} }', fields=ffilms)
$allcol_people$find(query='{"films": {"$all": [1,2,3]} }', fields=ffilms)
$sizecol_people$find(query='{"films": {"$size":3} }', fields=ffilms)
$type, $mod, $elemMatch,…)La méthode iterate prend les mêmes paramètres que find et retourne un itérateur pour parcourir le résultat de la recherche ligne par ligne à l'aide de $one (ou par page avec $page).
Une fois le dernier élément retourné, l'itérateur devient obsolète.
it <- col_people$iterate(
query='{"$and": [
{"height": {"$gt":160} }, {"mass": {"$lt":50} }
]}',
fields='{"_id":0, "name":1}',
sort='{"name":1}'
)
repeat {
item <- it$one()
if (!is.null(item)) message("Force et honneur, ", item$name, "!") else break
}
## Force et honneur, Padmé Amidala!
## Force et honneur, Sly Moore!
## Force et honneur, Wat Tambor!
Pour modifier un document de la collection, la méthode update procède en deux temps :
La recherche se fait à l'aide du paramètre query et les modifications avec update. Plusieurs commandes sont disponibles pour modifier le(s) document(s).
$setcol_people$update(query='{"name":"Luke Skywalker"}',
update='{"$set": {"hair_color":"blond, grey"} }')
col_people$find(query='{"name":"Luke Skywalker"}',
fields='{"_id":0, "name":1, "hair_color":1}')
$pushcol_people$update(query='{"name":"Luke Skywalker"}',
update='{"$push": {"films":8} }') # Spoiler !
col_people$find(query='{"name":"Luke Skywalker"}', fields=ffilms)
$addToSetcol_people$update(query='{"name":"Luke Skywalker"}',
update='{"$addToSet": {"films":8} }')
col_people$find(query='{"name":"Luke Skywalker"}', fields=ffilms)
$pullcol_people$update(query='{"name":"Luke Skywalker"}',
update='{"$pull": {"films":8} }')
col_people$find(query='{"name":"Luke Skywalker"}', fields=ffilms)
Comme d'habitude, attention aux fautes de frappes!
col_people$update(query='{"name":"Luke Skywalker"}',
update='{"$set": {"hair_colour":"blond, grey"} }')
col_people$find(query='{"name":"Luke Skywalker"}')
$unsetcol_people$update(query='{"name":"Luke Skywalker"}',
update='{"$unset": {"hair_colour": "blond, grey"} }')
col_people$find(query='{"name":"Luke Skywalker"}')
Et bien d'autres ($inc, $pop, $pushAll, $pullAll,…)
Pour appliquer les modifications à plusieurs documents, utiliser multiple=TRUE.
T'entraîner seul,
maintenant, tu dois.
La méthode count permet de compter des documents. Bref, la base…
pie(c(col_people$count('{"gender":"male"}'),
col_people$count('{"gender":"female"}'),
col_people$count('{"gender": {"$nin":["male","female"]} }')),
labels=c("Masculin", "Féminin", "Autres"), radius=1)
La méthode distinct retourne la liste des valeurs prises par une clé parmi les documents vérifiant une recherche donnée.
col_people$distinct(key="gender")
## [1] "male" "n/a" "female" "hermaphrodite" ## [5] "none"
col_people$distinct(key="hair_color", query='{"gender":"female"}')
## [1] "brown" "auburn" "black" "none" "blonde" "white" "unknown"
Comment compter les effectifs de différents groupes?
values <- col_people$distinct(key="gender"); count <- NULL
for (v in values) {
count <- c(count, col_people$count(paste0('{"gender":"', v, '"}')))
}
names(count) <- values; print(count)
## male n/a female hermaphrodite none ## 62 3 19 1 2
Comment compter les effectifs de différents groupes?
values <- col_people$distinct(key="gender"); count <- NULL
for (v in values) {
count <- c(count, col_people$count(paste0('{"gender":"', v, '"}')))
}
names(count) <- values; print(count)
## male n/a female hermaphrodite none ## 62 3 19 1 2
Comment compter les effectifs de différents groupes?
table(col_people$find(fields='{"_id":0,"gender":1}'))
## ## female hermaphrodite male n/a none ## 19 1 62 3 2
Comment compter les effectifs de différents groupes?
table(col_people$find(fields='{"_id":0,"gender":1}'))
## ## female hermaphrodite male n/a none ## 19 1 62 3 2
Un agrégateur est une fonction qui regroupe les valeurs contenues dans plusieurs documents sélectionnés et retourne une structure contenant des objets "simples" et "plus informatifs".
Les méthodes count et distinct sont des agrégateurs.
Les méthodes count et distinct sont des agrégateurs.
$match (filtre comme avec query),$group (regroupe et accumule),$sort (trie).$project, $limit, $skip, $sample, $out,…), voir :https://docs.mongodb.com/manual/reference/operator/aggregation-pipeline/
La méthode aggregate permet de contruire un agrégateur basé sur le modèle du pipeline d'agrégation. Cette méthode prend un tableau en paramètre contenant les différentes étapes à réaliser.
La méthode aggregate permet de contruire un agrégateur basé sur le modèle du pipeline d'agrégation. Cette méthode prend un tableau en paramètre contenant les différentes étapes à réaliser.
Exemple pour count :
col_people$aggregate('
[
{ "$match": { "hair_color": "white" } },
{ "$group": { "_id": null, "count": { "$sum": 1 } } }
]')
## _id count ## 1 NA 4
col_people$aggregate('
[
{ "$match": { "hair_color": "white" } },
{ "$group": { "_id": null, "count": { "$sum": 1 } } }
]')
$match est similaire à query,_id de $group reçoit la clé utilisée pour les groupes ou null pour considérer tous les documents,count de $group est le nom de l'accumulateur défini en suivant,Exemple pour distinct :
col_people$aggregate('
[
{ "$group": { "_id": "$gender" } }
]')
ou bien :
col_people$aggregate('
[
{ "$group": { "_id": null, "gender": { "$addToSet": "$gender" } } }
]')
Exemple pour compter des effectifs et les trier :
col_people$aggregate('
[
{ "$group": { "_id": "$gender", "count": { "$sum": 1 } } },
{ "$sort": { "count": -1 } }
]')
## _id count ## 1 male 62 ## 2 female 19 ## 3 n/a 3 ## 4 none 2 ## 5 hermaphrodite 1
Exemple un peu plus avancé :
col_people$aggregate('
[{ "$group": { "_id": "$gender",
"height": { "$avg": "$height" },
"mass": { "$avg": "$mass" } }
}]')
## _id height mass ## 1 none 200.0000 140.00000 ## 2 hermaphrodite 175.0000 1358.00000 ## 3 female 165.4706 54.02000 ## 4 n/a 120.0000 46.33333 ## 5 male 179.2373 81.00455
Exemple encore un peu plus avancé :
mydata <- col_people$aggregate('
[
{ "$project": { "height_grp": {
"$cond": [
{ "$lte": ["$height", 175] },
"Petit",
"Grand"
]
}
}
},
{ "$group": { "_id": "$height_grp", "count": { "$sum": 1 } } }
]')
https://docs.mongodb.com/manual/reference/operator/aggregation/
MongoDB propose une autre méthode, appelée Map-Reduce, pour réaliser des agrégations.
mapreduce.query, de trier le résultat avec sort et de le limiter avec limit.MongoDB propose une autre méthode, appelée Map-Reduce, pour réaliser des agrégations.
Map-Reduce offre plus de souplesse que le pipeline d'agrégation mais ce dernier doit rester le choix à privilégier car Map-Reduce est moins efficace et plus complexe en général.
Compter des effectifs avec Map-Reduce :
col_people$mapreduce(
'function() { emit(this.gender, 1) }',
'function(key, values) { return Array.sum(values) }'
)
## _id value ## 1 female 19 ## 2 hermaphrodite 1 ## 3 male 62 ## 4 n/a 3 ## 5 none 2
Mesurer la longueur moyenne des noms en fonction du genre :
col_people$mapreduce(
'function() { emit(this.gender, this.name.length) }',
'function(key, values) { return Array.sum(values) / values.length }'
)
## _id value ## 1 female 10.10526 ## 2 hermaphrodite 21.00000 ## 3 male 10.62903 ## 4 n/a 5.00000 ## 5 none 4.00000
height_range <- col_people$aggregate('[
{ "$group": { "_id": null,
"min": { "$min": "$height" },
"max": { "$max": "$height" } }
}
]')
n <- 5; step <- (height_range$max - height_range$min) / n
mydata <- col_people$mapreduce(
paste0('function() {
var cat = Math.floor((this.height - ', height_range$min, ')/', step, ');
emit("Cat" + cat.toString(), 1)
}'),
'function(key, values) { return Array.sum(values) }'
)
val <- mydata$value[1:n] val[n] <- val[n] + mydata$value[n + 1] barplot(val, col="yellow", space=0) axis(1, at=0:n, labels=height_range$min+(0:n)*step)
T'entraîner seul,
maintenant, tu dois.
Merci à toutes et à tous !
Bonnes Rencontres R 2016 !